package nr.co.mhgames.polyanim;

import java.util.ArrayList;

import nr.co.mhgames.polyanim.util.Matrix;
import nr.co.mhgames.polyanim.util.Vector2f;

/**
 * Polygon group. Renderer must make sure the group is transformed correctly, by
 * either supporting the transformations natively (such as Java2D and LWJGL
 * renderers do), or making use of the transformation matrix that can be
 * optionally computed (see {@link #computeTransforms()}).
 * <p>
 * Note: this class has a natural ordering that is inconsistent with equals,
 * since the Groups are compared using their depth value only.
 * 
 * @author Mika Halttunen
 */
public class Group implements Comparable<Group> {
	/** Group name */
	protected String name;

	/** List of polygons */
	protected ArrayList<Polygon> polys;

	/** Group depth value. The groups are drawn in order, sorted by depth. */
	protected int depthValue;

	/** Group visibility */
	protected boolean visible;


	/** Group offset in XY plane */
	protected Vector2f offset;

	/** Group scale */
	protected Vector2f scale;

	/** Pivot point for rotation */
	protected Vector2f pivot;

	/** Rotation angle in degrees */
	protected float rotationAngle;

	// TODO: Add blending parameters: alpha, additive and none


	/** Default constructor */
	public Group() {
		name = "null";
		polys = new ArrayList<Polygon>();
		visible = true;

		depthValue = 0;
		try {
			offset = (Vector2f) Vector2f.ZERO.clone();
			scale = (Vector2f) Vector2f.UNIT.clone();
			pivot = (Vector2f) Vector2f.ZERO.clone();
		} catch (CloneNotSupportedException ex) {
		}

		rotationAngle = 0;
	}

	/** Constructor */
	public Group(String name) {
		this();
		this.name = name;
	}

	/**
	 * Another constructor
	 * 
	 * @param name
	 *            Group name
	 * @param polygons
	 *            List of polygons
	 */
	public Group(String name, ArrayList<Polygon> polygons) {
		this();
		this.name = name;
		this.polys = polygons;
	}

	/**
	 * Another constructor
	 * 
	 * @param name
	 *            Group name
	 * @param polygons
	 *            Array of polygons
	 */
	public Group(String name, Polygon... polygons) {
		this();
		this.name = name;
		this.polys.clear();
		for (Polygon p: polygons)
			polys.add(p);
	}


	/** Return the group name */
	public String getName() {
		return name;
	}

	/**
	 * Set the group name.
	 * 
	 * @param name
	 *            Group name
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Return the group visibility. Invisible groups will not be rendered,
	 * naturally.
	 */
	public boolean isVisible() {
		return visible;
	}

	/**
	 * Set the group visibility.
	 * 
	 * @param vis
	 *            Visible or not?
	 */
	public void setVisible(boolean vis) {
		visible = vis;
	}


	/**
	 * Add a polygon to the group.
	 * 
	 * @param poly
	 *            Polygon to add
	 */
	public void addPolygon(Polygon poly) {
		polys.add(poly);
	}

	/**
	 * Return the group polygons.
	 * 
	 * @return List of polygons
	 */
	public ArrayList<Polygon> getPolygons() {
		return polys;
	}

	/**
	 * Return the polygon at given index.
	 * 
	 * @param i
	 *            Polygon index
	 * @return Polygon at the index
	 */
	public Polygon getPolygon(int i) {
		return polys.get(i);
	}

	/**
	 * Set the given polygon to the specified index.
	 * 
	 * @param i
	 *            Polygon index
	 * @param poly
	 *            New polygon
	 */
	public void setPolygon(int i, Polygon poly) {
		polys.set(i, poly);
	}

	/**
	 * Insert the given polygon to the specified index. The polygons already in
	 * the group are shifted to make room for the new polygon.
	 * 
	 * @param i
	 *            Index to insert to
	 * @param poly
	 *            New polygon
	 */
	public void insertPolygon(int i, Polygon poly) {
		polys.add(i, poly);
	}

	/**
	 * Replace all the group polygons with the given list of new polygons.
	 * 
	 * @param polys
	 *            New polygons
	 */
	public void replacePolygons(ArrayList<Polygon> polys) {
		this.polys = polys;
	}

	/**
	 * Return the number of polygons in the group.
	 * 
	 * @return Number of polygons
	 */
	public int getPolygonCount() {
		return polys.size();
	}

	// //////////////////////
	// Transformations
	// //////////////////////

	/**
	 * Computes the transformation matrix for the group. The matrix can be used
	 * in rendering, if needed.
	 * 
	 * @return Group transformation matrix
	 */
	public Matrix computeTransforms() {
		Matrix res = new Matrix();
		res.loadIdentity();

		// Translate to the offset
		res.translate(offset.x, offset.y);

		// Rotate around the pivot point
		if (rotationAngle != 0.0f) {
			res.translate(pivot.x, pivot.y);
			res.rotateDegree(rotationAngle);
			res.translate(-pivot.x, -pivot.y);
		}

		// Scale
		// TODO: Benchmark the conditional vs. always scaling!
		if (scale.x != 1.0f || scale.y != 1.0f)
			res.scale(scale.x, scale.y);

		return res;
	}

	/**
	 * Return the group translation offset in XY plane.
	 * 
	 * @return Translation offset
	 */
	public Vector2f getOffset() {
		return offset;
	}

	/**
	 * Set the group translation offset in XY plane.
	 * 
	 * @param offset
	 *            New translation offset
	 */
	public void setOffset(Vector2f offset) {
		this.offset = offset;
	}

	/**
	 * Set the group translation offset in XY plane.
	 * 
	 * @param x
	 *            X translation
	 * @param y
	 *            Y translation
	 */
	public void setOffset(float x, float y) {
		this.offset = new Vector2f(x, y);
	}


	/**
	 * Return the group scaling factor.
	 * 
	 * @return Scaling factor
	 */
	public Vector2f getScale() {
		return scale;
	}

	/**
	 * Set the group scaling factor.
	 * 
	 * @param scale
	 *            Scale vector in XY
	 */
	public void setScale(Vector2f scale) {
		this.scale = scale;
	}

	/**
	 * Set the group scaling factor.
	 * 
	 * @param x
	 *            X scale
	 * @param y
	 *            Y scale
	 */
	public void setScale(float x, float y) {
		this.scale = new Vector2f(x, y);
	}

	/**
	 * Set the group scaling factor.
	 * 
	 * @param scale
	 *            Scale in XY
	 */
	public void setScale(float scale) {
		this.scale = new Vector2f(scale);
	}

	/**
	 * Return the pivot point for rotation.
	 * 
	 * @return Pivot point
	 */
	public Vector2f getPivot() {
		return pivot;
	}

	/**
	 * Set the pivot point for rotation.
	 * 
	 * @param pivot
	 *            New pivot point
	 */
	public void setPivot(Vector2f pivot) {
		this.pivot = pivot;
	}

	/**
	 * Set the pivot point for rotation.
	 * 
	 * @param x
	 *            Pivot point X
	 * @param y
	 *            Pivot point Y
	 */
	public void setPivot(float x, float y) {
		this.pivot = new Vector2f(x, y);
	}


	/**
	 * Return the group rotation angle, in degrees.
	 * 
	 * @return Rotation angle
	 */
	public float getRotation() {
		return rotationAngle;
	}

	/**
	 * Set the group rotation angle, in degrees.
	 * 
	 * @param rotation
	 *            New rotation angle
	 */
	public void setRotation(float rotation) {
		this.rotationAngle = rotation;
	}

	// //////////////////

	/**
	 * Return the group depth value.
	 * 
	 * @return Depth value
	 */
	public int getDepthValue() {
		return depthValue;
	}

	/**
	 * Set the group depth value.
	 * 
	 * @param depth
	 *            New depth value
	 */
	public void setDepthValue(int depth) {
		depthValue = depth;
	}

	/**
	 * Comparison by depth values.
	 */
	public int compareTo(Group o) {
		return depthValue - o.depthValue;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + depthValue;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + ((offset == null) ? 0 : offset.hashCode());
		result = prime * result + ((pivot == null) ? 0 : pivot.hashCode());
		result = prime * result + ((polys == null) ? 0 : polys.hashCode());
		result = prime * result + Float.floatToIntBits(rotationAngle);
		result = prime * result + ((scale == null) ? 0 : scale.hashCode());
		result = prime * result + (visible ? 1231 : 1237);
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (!(obj instanceof Group))
			return false;

		Group other = (Group) obj;
		if (depthValue != other.depthValue)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;

		if (offset == null) {
			if (other.offset != null)
				return false;
		} else if (!offset.equals(other.offset))
			return false;

		if (pivot == null) {
			if (other.pivot != null)
				return false;
		} else if (!pivot.equals(other.pivot))
			return false;

		if (polys == null) {
			if (other.polys != null)
				return false;
		} else if (!polys.equals(other.polys))
			return false;

		if (Float.floatToIntBits(rotationAngle) != Float.floatToIntBits(other.rotationAngle))
			return false;

		if (scale == null) {
			if (other.scale != null)
				return false;
		} else if (!scale.equals(other.scale))
			return false;

		if (visible != other.visible)
			return false;

		return true;
	}

	/**
	 * Interpolate between two polygon groups A and B. Both groups must have
	 * exactly the same polygons, i.e. A's polygon #1 corresponds to the #1 in
	 * B, and so forth. That doesn't of course mean the polygon vertices must be
	 * in the same locations!
	 * 
	 * @param a
	 *            Polygon group A
	 * @param b
	 *            Polygon group B
	 * @param t
	 *            Interpolation factor. Zero means the resulting group is A,
	 *            while one means the resulting group is B. Anything between is
	 *            interpolated.
	 * @return An interpolated polygon group between A and B.
	 */
	public static Group interpolate(Group a, Group b, float t) {
		assert (a.getPolygonCount() == b.getPolygonCount());
		if (a == b) {
			// It's the same group, don't interpolate
			return a;
		}

		if (t <= 0) {
			// Don't interpolate at all (result is A)
			return a;
		} else if (t >= 1.0f) {
			// Interpolate fully (result is B)
			return b;
		}

		// Reserve space for the new group
		ArrayList<Polygon> polys = new ArrayList<Polygon>(a.getPolygonCount());
		Group res = new Group(a.getName(), polys);

		// Interpolate the transforms
		// Offset:
		float dX = b.offset.x - a.offset.x;
		float dY = b.offset.y - a.offset.y;
		res.offset.x = a.offset.x + t * dX;
		res.offset.y = a.offset.y + t * dY;

		// Scale:
		dX = b.scale.x - a.scale.x;
		dY = b.scale.y - a.scale.y;
		res.scale.x = a.scale.x + t * dX;
		res.scale.y = a.scale.y + t * dY;

		// Pivot:
		dX = b.pivot.x - a.pivot.x;
		dY = b.pivot.y - a.pivot.y;
		res.pivot.x = a.pivot.x + t * dX;
		res.pivot.y = a.pivot.y + t * dY;

		// Rotation:
		float diffR = b.rotationAngle - a.rotationAngle;
		res.rotationAngle = a.rotationAngle + t * diffR;

		// "Interpolate" between visible and invisible, as well as between
		// the depth values defined in A and B.
		if (t >= 0.5f) {
			res.visible = b.visible;
			res.depthValue = b.depthValue;
		} else {
			res.visible = a.visible;
			res.depthValue = a.depthValue;
		}

		// Interpolate the polygons
		int polyCount = a.getPolygonCount();
		for (int i = 0; i < polyCount; ++i) {
			Polygon pA = a.getPolygon(i);
			Polygon pB = b.getPolygon(i);

			Polygon p = Polygon.interpolate(pA, pB, t);
			res.polys.add(p);
		}

		return res;
	}


	/**
	 * Checks that the corresponding polygons in two groups match.
	 * 
	 * @param a
	 *            Group A
	 * @param b
	 *            Group B
	 * @throws Exception
	 *             Thrown if the check fails
	 */
	public static void checkForFaults(Group a, Group b) throws Exception {
		// Check A vs B
		int polyCount = a.getPolygonCount();
		for (int i = 0; i < polyCount; ++i) {
			if (a.getPolygon(i).getVertexCount() != b.getPolygon(i).getVertexCount())
				throw new Exception(String.format("Error between corresponding groups %s:\n"
						+ "Polygon %d has %d vertices in first group,"
						+ " but %d vertices in the second!", a.getName(), i, a.getPolygon(i)
						.getVertexCount(), b.getPolygon(i).getVertexCount()));
		}

		// Check B vs A
		polyCount = b.getPolygonCount();
		for (int i = 0; i < polyCount; ++i) {
			if (b.getPolygon(i).getVertexCount() != a.getPolygon(i).getVertexCount())
				throw new Exception(String.format("Error between corresponding groups %s:\n"
						+ "Polygon %d has %d vertices in first group,"
						+ " but %d vertices in the second!", a.getName(), i, a.getPolygon(i)
						.getVertexCount(), b.getPolygon(i).getVertexCount()));
		}
	}


	@Override
	public String toString() {
		return String.format("Group %s at %s: %d polygons, %s", getName(), getOffset().toString(),
				getPolygonCount(), super.toString());
	}
}
